﻿#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Transactions;
using Microsoft.Extensions.Options;
using NetMicro.Core.Helper;
using NetMicro.EventBus.Diagnostics;
using NetMicro.EventBus.Extensions;
using NetMicro.EventBus.Messages;
using NetMicro.Trace.Propagator;

namespace NetMicro.EventBus.Publishers
{
    public class EventPublisher : IPublisherEvent
    {
        public EventPublisher(

            IMessageSerializer messageSerializer,
            IEventNameRuler eventNameRuler,
            IOptions<EventBusOptions> options,
            IMsgIdGenerator msgIdGenerator, IMessageSender messageSender)
        {

            MessageSerializer = messageSerializer;
            EventNameRuler = eventNameRuler;
            Options = options;
            MsgIdGenerator = msgIdGenerator;
            MessageSender = messageSender;
        }
        protected static readonly DiagnosticListener s_diagnosticListener =
          new DiagnosticListener(DiagnosticListenerNames.DiagnosticListenerName);

        private static readonly ActivitySource EventPublisherActivitySource = new ActivitySource(nameof(EventPublisher));
        private static readonly TraceContextPropagator EventPublisherPropagator = new TraceContextPropagator();

        private IMessageSender MessageSender { get; }
        private IMessageSerializer MessageSerializer { get; }

        private IEventNameRuler EventNameRuler { get; }
        private IMsgIdGenerator MsgIdGenerator { get; }
        private IOptions<EventBusOptions> Options { get; }

        public async Task PublishAsync<TEvent>(
            TEvent @event,
            IDictionary<string, string>? additionalItems = null,
            CancellationToken cancellationToken = default
        ) where TEvent : IEvent
        {
            await Publish(@event, additionalItems, cancellationToken).ConfigureAwait(false);
        }
        private async Task Publish<TEvent>(
            TEvent @event,
            IDictionary<string, string>? additionalItems = null,
            CancellationToken cancellationToken = default
        )
        {

            if (@event is null) throw new ArgumentNullException(nameof(@event));
            var now = DateTimeOffset.Now;
            var eventName = EventNameRuler.GetName(typeof(TEvent));
            
            using (var activity = EventPublisherActivitySource.StartActivity(eventName, ActivityKind.Producer))
            {
                additionalItems ??= new Dictionary<string, string>();
                ActivityContext contextToInject = default;
                if (activity != null)
                {
                    contextToInject = activity.Context;
                }
                else if (Activity.Current != null)
                {
                    contextToInject = Activity.Current.Context;
                }
                EventPublisherPropagator.Inject(contextToInject, additionalItems, TraceContext.InjectTraceContextIntoBasicProperties);
                var msgId = MsgIdGenerator.GenerateId();
                additionalItems.Add(EventBusConsts.EventNameHeaderKey, eventName);
                additionalItems.Add(EventBusConsts.MsgIdHeaderKey, msgId);
                long? tracingTimestamp = null;
                MessageTransfer messageTransfer = new MessageTransfer
                {
                    MsgId = msgId,
                    Version = Options.Value.Version,
                    SendAt = now,
                    EventName = EventNameRuler.GetName(typeof(TEvent)),
                    Items = additionalItems,
                    MsgBody = MessageSerializer.Serialize(@event)
                };
                try
                {
                    tracingTimestamp = TracingBefore(messageTransfer);
                    await MessageSender.Send(messageTransfer).ConfigureAwait(false);
                    TracingAfter(tracingTimestamp, messageTransfer);
                }
                catch (Exception ex)
                {
                    TracingError(tracingTimestamp, messageTransfer, ex);
                    throw;
                }
            }
        }
        #region tracing

        private long? TracingBefore(MessageTransfer message)
        {
            if (s_diagnosticListener.IsEnabled(DiagnosticListenerNames.BeforePub))
            {
                var eventData = new TraceEventData()
                {
                    OperationTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
                    Operation = message.EventName,
                    Message = message
                };

                s_diagnosticListener.Write(DiagnosticListenerNames.BeforePub, eventData);
                return eventData.OperationTimestamp;
            }

            return null;
        }

        private void TracingAfter(long? tracingTimestamp, MessageTransfer message)
        {
            if (tracingTimestamp != null && s_diagnosticListener.IsEnabled(DiagnosticListenerNames.AfterPub))
            {
                var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                var eventData = new TraceEventData()
                {
                    OperationTimestamp = now,
                    Operation = message.EventName,
                    Message = message,
                    ElapsedTimeMs = now - tracingTimestamp.Value
                };

                s_diagnosticListener.Write(DiagnosticListenerNames.AfterPub, eventData);
            }
        }

        private void TracingError(long? tracingTimestamp, MessageTransfer message, Exception ex)
        {
            if (tracingTimestamp != null && s_diagnosticListener.IsEnabled(DiagnosticListenerNames.ErrorPub))
            {
                var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
                var eventData = new TraceEventData()
                {
                    OperationTimestamp = now,
                    Operation = message.EventName,
                    Message = message,
                    ElapsedTimeMs = now - tracingTimestamp.Value,
                    Exception = ex
                };

                s_diagnosticListener.Write(DiagnosticListenerNames.ErrorPub, eventData);
            }
        }

        #endregion
    }
}